/* This file is part of swapper project
 *
 * Copyright (C) 2020 The Swapper Project Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.swapper.reflect;

import java.io.Serializable;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Objects;

/**
 * Type parser encapsulation.
 * Part of the source code and inspiration came from Gson.
 * <p>
 * {@code Type listType = new TypeParser<List<String>>() {}.getType();}
 * {@code Type mapType = new TypeParser<Map<Integer, String>>() {}.getType();}
 *
 * @param <T> the actual type that needs to be resolved.
 */
public abstract class TypeParser<T> {
  private final Type type;
  private final Class<? super T> rawType;

  /**
   * Gets the actual parsed type from the generic parameter of
   * the anonymous inner class.
   * This constructor will work in a subclass.
   */
  protected TypeParser() {
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof ParameterizedType) {
      // Super class first actualTypeArguments is type Of <T>.
      type = proxy(((ParameterizedType) superclass).getActualTypeArguments()[0]);
      rawType = TypeHelper.getRawType(type);
    } else {
      throw new IllegalStateException("Missing type parameter.");
    }
  }

  /**
   * The private constructor.
   * Gets the actual parsed type from the specified type.
   *
   * @param type the specified type.
   * @throws NullPointerException if the specified type is null.
   */
  private TypeParser(Type type) {
    this.type = proxy(type);
    this.rawType = TypeHelper.getRawType(this.type);
  }

  /**
   * Gets the type.
   * This type is a substitute type.
   *
   * @return the type.
   */
  public final Type getType() {
    return type;
  }

  /**
   * Gets the raw type.
   * An instantiable type.
   *
   * @return the raw type.
   */
  public final Class<? super T> getRawType() {
    return rawType;
  }

  /**
   * Indicates whether some other object is "equal to" this one.
   *
   * @param other the reference object with which to compare.
   * @return returns true if this object is the same as the obj argument; false otherwise.
   * @see TypeHelper#equals(Type, Type)
   */
  @Override
  public final boolean equals(Object other) {
    return other instanceof TypeParser<?> && TypeHelper.equals(type, ((TypeParser<?>) other).type);
  }

  /**
   * Gets the hash code of the current instance.
   *
   * @return the hash code of the current instance.
   * @see Object#hashCode()
   */
  @Override
  public final int hashCode() {
    return type.hashCode();
  }

  /**
   * Gets the string of the current instance.
   *
   * @return the string of the current instance.
   * @see TypeHelper#name(Type)
   */

  @Override
  public final String toString() {
    return TypeHelper.name(type);
  }

  /**
   * Gets a new type that replaces the function of the specified type.
   *
   * @param type the specified type.
   * @return a new substitute type.
   * @throws NullPointerException if the specified type is null.
   */
  public static Type proxy(Type type) {
    if (type instanceof Class) {
      Class<?> c = (Class<?>) type;
      return c.isArray() ? new GenericArrayTypeProxy(proxy(c.getComponentType())) : c;
    } else if (type instanceof ParameterizedType) {
      ParameterizedType p = (ParameterizedType) type;
      return new ParameterizedTypeProxy(p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
    } else if (type instanceof GenericArrayType) {
      GenericArrayType g = (GenericArrayType) type;
      return new GenericArrayTypeProxy(g.getGenericComponentType());
    } else if (type instanceof WildcardType) {
      WildcardType w = (WildcardType) type;
      return new WildcardTypeProxy(w.getUpperBounds(), w.getLowerBounds());
    } else {
      return Objects.requireNonNull(type);
    }
  }

  /**
   * Static constructor.
   * Construct a parse type from the specified type.
   *
   * @param type the specified type.
   * @return a parse type from the specified type.
   * @throws NullPointerException if the specified type is null.
   */
  public static <T> TypeParser<T> typeOf(Type type) {
    return new TypeParser<T>(type) {
    };
  }

  /**
   * Static constructor.
   * Construct a parse type from the specified parameterized type.
   *
   * @param ownerType     the owner type, {@code null} if it is not an inner class.
   * @param rawType       the parameterized raw type.
   * @param typeArguments the parameterized type arguments.
   * @return a parse type from the specified generic type.
   * @throws NullPointerException     if the specified raw type or type arguments is null.
   * @throws IllegalArgumentException 1.if the specified raw type is inner class, but the owner type is null;
   *                                  2.if the specified type arguments is empty;
   *                                  3.if the type arguments contains any primitive type.
   */
  public static <T> TypeParser<T> parameterizedOf(Type ownerType, Type rawType, Type... typeArguments) {
    return new TypeParser<T>(new ParameterizedTypeProxy(ownerType, rawType, typeArguments)) {
    };
  }

  /**
   * Static constructor.
   * Construct a parse type from the specified array type.
   *
   * @param componentType the component type of the specified array type.
   * @return a parse type from the specified array type.
   */
  public static <T> TypeParser<T> arrayOf(Type componentType) {
    return new TypeParser<T>(new GenericArrayTypeProxy(componentType)) {
    };
  }

  /**
   * Proxy java {@link ParameterizedType} interface implementation class.
   * {@code List<E> a; Map<K,V> b; Bean<T> c;} ...
   */
  private static final class ParameterizedTypeProxy implements ParameterizedType, Serializable {
    private final Type mOwnerType;
    private final Type mRawType;
    private final Type[] mTypeArguments;
    private static final long serialVersionUID = 0;

    /**
     * constructor.
     * Construct a parse type from the specified parameterized type.
     *
     * @param ownerType     the owner type, {@code null} if it is not an inner class.
     * @param rawType       the parameterized raw type.
     * @param typeArguments the parameterized type arguments.
     * @throws NullPointerException     if the specified raw type or type arguments is null.
     * @throws IllegalArgumentException 1.if the specified raw type is inner class, but the owner type is null;
     *                                  2.if the specified type arguments is empty;
     *                                  3.if the type arguments contains any primitive type.
     */
    public ParameterizedTypeProxy(Type ownerType, Type rawType, Type... typeArguments) {
      if (rawType instanceof Class<?>) {
        if (ownerType == null && !Modifier.isStatic(((Class<?>) rawType).getModifiers())
          && ((Class<?>) rawType).getEnclosingClass() != null) {
          throw new IllegalArgumentException("The inner class must specify an owner.");
        }
      }
      mOwnerType = ownerType == null ? null : proxy(ownerType);
      mRawType = proxy(rawType);
      mTypeArguments = typeArguments.clone();
      if (mTypeArguments.length < 1) {
        throw new IllegalArgumentException("The specified type arguments is empty.");
      }
      for (int i = 0; i < mTypeArguments.length; ++i) {
        if (TypeHelper.isPrimitive(mTypeArguments[i])) {
          throw new IllegalArgumentException("Generic types do not support primitive types.");
        }
        mTypeArguments[i] = proxy(mTypeArguments[i]);
      }
    }

    @Override
    public Type[] getActualTypeArguments() {
      return mTypeArguments.clone();
    }

    @Override
    public Type getRawType() {
      return mRawType;
    }

    @Override
    public Type getOwnerType() {
      return mOwnerType;
    }

    @Override
    public boolean equals(Object other) {
      return other instanceof ParameterizedType
        && TypeHelper.equals(this, (ParameterizedType) other);
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(mTypeArguments) ^ mRawType.hashCode()
        ^ (mOwnerType == null ? 0 : mOwnerType.hashCode());
    }

    @Override
    public String toString() {
      int length = mTypeArguments.length;
      if (length == 0) {
        return TypeHelper.name(mRawType);
      }
      StringBuilder builder = new StringBuilder(30 * (length + 1));
      builder.append(TypeHelper.name(mRawType))
        .append("<").append(TypeHelper.name(mTypeArguments[0]));
      for (int i = 1; i < length; i++) {
        builder.append(",").append(TypeHelper.name(mTypeArguments[i]));
      }
      return builder.append(">").toString();
    }
  }

  /**
   * Proxy java {@link GenericArrayType} interface implementation class.
   * {@code T[] a; List<T>[] b; Bean<T>[]} ...
   */
  private static final class GenericArrayTypeProxy implements GenericArrayType, Serializable {
    private final Type mComponentType;
    private static final long serialVersionUID = 0;

    public GenericArrayTypeProxy(Type componentType) {
      mComponentType = proxy(componentType);
    }

    public Type getGenericComponentType() {
      return mComponentType;
    }

    @Override
    public boolean equals(Object other) {
      return other instanceof GenericArrayType && TypeHelper.equals(this, ((GenericArrayType) other));
    }

    @Override
    public int hashCode() {
      return mComponentType.hashCode();
    }

    @Override
    public String toString() {
      return TypeHelper.name(mComponentType) + "[]";
    }
  }

  /**
   * Proxy java {@link WildcardType} interface implementation class.
   * {@code List<? extends Number> a; List<? super Integer> b;}
   */
  private static final class WildcardTypeProxy implements WildcardType, Serializable {
    private final Type mUpperBound;
    private final Type mLowerBound;
    private static final long serialVersionUID = 0;
    private static final Type[] EMPTY_TYPE_ARRAY = new Type[]{};

    public WildcardTypeProxy(Type[] upperBounds, Type[] lowerBounds) {
      if (lowerBounds.length > 1) {
        throw new IllegalArgumentException("WildcardType lower bound should <= 1.");
      }
      if (upperBounds.length != 1) {
        throw new IllegalArgumentException("WildcardType upper bound should == 1.");
      }
      if (lowerBounds.length == 1) {
        Objects.requireNonNull(lowerBounds[0]);
        if (TypeHelper.isPrimitive(lowerBounds[0])) {
          throw new IllegalArgumentException("Generic types do not support primitive types.");
        }
        if (upperBounds[0] != Object.class) {
          throw new IllegalArgumentException("WildcardType upper bounds must be Object.class.");
        }
        mLowerBound = proxy(lowerBounds[0]);
        mUpperBound = Object.class;
      } else {
        Objects.requireNonNull(upperBounds[0]);
        if (TypeHelper.isPrimitive(upperBounds[0])) {
          throw new IllegalArgumentException("Generic types do not support primitive types.");
        }
        mLowerBound = null;
        mUpperBound = proxy(upperBounds[0]);
      }
    }

    @Override
    public Type[] getUpperBounds() {
      return new Type[]{mUpperBound};
    }

    @Override
    public Type[] getLowerBounds() {
      return mLowerBound != null ? new Type[]{mLowerBound} : EMPTY_TYPE_ARRAY;
    }

    @Override
    public boolean equals(Object other) {
      return other instanceof WildcardType && TypeHelper.equals(this, (WildcardType) other);
    }

    @Override
    public int hashCode() {
      return (mLowerBound != null ? 31 + mLowerBound.hashCode() : 1) ^ (31 + mUpperBound.hashCode());
    }

    @Override
    public String toString() {
      if (mLowerBound != null) {
        return "? super " + TypeHelper.name(mLowerBound);
      } else if (mUpperBound == Object.class) {
        return "?";
      } else {
        return "? extends " + TypeHelper.name(mUpperBound);
      }
    }
  }
}
